"""Cellphone multiplexer. Handles SMS and GPRS functions."""

import fcntl
import logging
import os
import re
import select
import string
import time
import termios

log = logging.getLogger('modem')

class IOError(Exception): pass
class IOTimeout(Exception): pass
class ModemError(Exception): pass

class ModemConfig:
    DEF_CFLAG   = ['!CSIZE', 'CS8', '!PARENB', 'CRTSCTS'] # This is 8N1
    DEF_LFLAG   = ['!ECHO', '!ICANON']
                         
    def __init__(self, device_file,
                 baud_rate    = termios.B230400,
                 serial_cflag = None,
                 serial_lflag = None,
                 timeout      = None):
        self.device_file = device_file
        self.baud_rate   = baud_rate
        if not timeout:
            timeout = { 'default'  : 1.0,
                        'read'     : 1.0,
                        'write'    : 1.0,
                        'send_pdu' : 60 }
        self.timeout = timeout
        self.name    = 'noname'

        # Default is 8N1
        self.serial_cflag = serial_cflag or self.DEF_CFLAG
        self.serial_lflag = serial_lflag or self.DEF_LFLAG

    @staticmethod
    def str2baud(str):
        return getattr(termios, 'B' + str)
        
class Modem:
    IFLAG, OFLAG, CFLAG, LFLAG, ISPEED, OSPEED, CC = range(7)
    
    def __init__(self, cfg):
        self.cfg = cfg
        self.modem_fd = None
        self.modem_io = None

    @staticmethod
    def configure_term_flags(flags, settings):
        for s in settings:
            if s[0] == '!':
                flags = flags & ~getattr(termios, s[1:])
            else:
                flags = flags | getattr(termios, s)
        return flags
    
    def open(self):
	self.modem_fd = os.open(self.cfg.device_file,
                                os.O_RDWR | os.O_NOCTTY | os.O_NDELAY,
                                0777)
    
	settings = termios.tcgetattr(self.modem_fd)
        # constants for indexing into the termio settings
        settings[self.OSPEED] = self.cfg.baud_rate
	settings[self.ISPEED] = self.cfg.baud_rate
        settings[self.CFLAG]  = self.configure_term_flags(settings[self.CFLAG],
                                                          self.cfg.serial_cflag)
        settings[self.LFLAG]  = self.configure_term_flags(settings[self.LFLAG],
                                                          self.cfg.serial_lflag)
        settings[self.OFLAG] = 0
        termios.tcsetattr(self.modem_fd, termios.TCSANOW, settings)

	fcntl.fcntl(self.modem_fd, fcntl.F_SETFL, os.O_NONBLOCK)
	self.modem_io = os.fdopen(self.modem_fd)

    def close(self):
        self.modem_io.close()
        os.close(self.modem_fd)
        self.modem_fd = None
        self.modem_io = None
        
    def write(self, str, timeout = None):
        timeout = timeout or self.cfg.timeout['write']

        while len(str)>0:
            _, w, _ = select.select([], [self.modem_fd], [], timeout)
            if len(w) == 0:
                raise IOTimeout('write: timeout %f waiting for modem' % timeout)
            r = os.write(self.modem_fd, str[0])
            if r < 0:
                raise IOError("Couldn't write: '%s' to modem" % text)
            self.modem_io.flush()
            str = str[1:]

    def read_until(self, pat, timeout = None):
        """Read input until we get util_pat on a line"""
        timeout = timeout or self.cfg.timeout['read']
        
        rex = re.compile(pat, re.M)
        ret = ''

        while True:
            r, _, _ = select.select([self.modem_fd], [], [], timeout)
            if len(r) == 0:
                raise IOTimeout('read_until: select timeout after %f looking for %s' %
                                (self.cfg.timeout['read'], pat))
            c = os.read(self.modem_fd, 1)
            ret = ret + c

            if rex.search(ret):
                break
        return ret

class ModemControl:
    """Note: This command set is based on the documentation for the
    Falcom 75 modem using the Siemens MC75 GSM modem. Compatibility
    may be different for other phones."""
    
    CMD_RET   = '^(OK|ERROR)\r\n'
    RESP_HEAD = r'^[+\^]([a-zA-Z]+): *(.*)'

    SMS_UNREAD, SMS_READ, SMS_UNSENT, SMS_SENT, SMS_ALL = range(5)
    SMS_MODEM_STORAGE, SMS_SIM_STORAGE, SMS_COMBINE_STORAGE = 'ME', 'SM', 'MT'
    
    def __init__(self, modem):
        self.modem = modem

        if self.modem.modem_fd == None:
            raise ModemError("modem isn't open!")
        
        self.reset_modem()
        self.set_modem_echo(False)

    def send_cmd(self, cmd, timeout = None):
        log.debug("modem command: '%s'" % cmd)
        self.modem.write(cmd + '\r\n')
        ret = self.modem.read_until(self.CMD_RET, timeout)
        ret = [i.strip() for i in ret.split('\r\n') if i.strip() != '']
        log.debug("modem command ret: '%s'" % ret)
        if ret[-1] == 'ERROR':
            raise ModemError('Error exectuting: "%s"' % cmd)
        return ret[:-1]

    def send_ctrl_z(self):
        self.modem.write('%c' % 0x1A)

    @staticmethod
    def parse_resp_line(l):
        rex = re.compile(ModemControl.RESP_HEAD)
        if l[0] == '+' or l[0] == '^':
            m = rex.match(l)
            return m.group(1), m.group(2)
        else:
            return None, l
       
    def reset_modem(self):
        self.send_cmd('ATZ', 0.5)

    def set_modem_echo(self, b):
        if b: self.send_cmd('ATE=1')
        else: self.send_cmd('ATE')

    def idle_ping(self):
        self.send_cmd('AT')

    def get_hardware_model(self):
        manu = self.send_cmd('AT+CGMI')
        model = self.send_cmd('AT+CGMM')
        rev = self.send_cmd('AT+CGMR')
        return {'manufacturer' : manu[0], 'model' : model[0], 'rev' : rev[0]}

    def get_IMEI(self):
        return self.send_cmd('AT+CGSN')[0]

    def get_signal_quality(self):
        """Returns (rssi, ber)
        rssi:

        0     -113 dBm or less
        1     -111 dBm
        2..30 -109... -53 dBm
        31    -51 dBm or greater
        99    not known or not detectable

        ber: 0..7 as RXQUAL values in the table in GSM 05.08 section 8.2.4.
        """
        r = self.send_cmd('AT+CSQ')
        return r[0].split(':')[1].strip().split(',')

    def get_celltower_info(self):
        r = self.send_cmd('AT^SMOND')
        ## XXX/bowei -- parse this later
        return r

    def get_gprs_info(self):
        r = self.send_cmd('AT^SMONG')
        ## XXX?bowei -- parse this later
        return r

    def delete_sms_msg(self, id):
        self.send_cmd('AT+CMGD=%d' % id)

    def set_sms_fmt(self, fmt):
        """fmt: 0 - PDU, 1 - text"""
        self.send_cmd('AT+CMGF=%d' % fmt)
    
    def list_sms_msgs(self, stat = None):
        if stat is None:
            stat = ModemControl.SMS_ALL
        r = self.send_cmd('AT+CMGL=%d' % stat)
        l = []
        if len(r) > 0 and r[0] == ('AT+CMGL=%d' % stat):
            # XXX/bowei - sometimes I'm getting an extra AT+CMGL=X
            # being written back by the modem
            r = r[1:]
        for meta, pdu in [(self.parse_resp_line(r[i*2]), r[i*2+1])
                          for i in range(len(r)/2)]:
            meta = dict(zip(['index', 'stat', 'alpha', 'len'], meta[1].split(',')))
            meta['index'] = int(meta['index'])
            meta['len'] = int(meta['len'])
            l.append((meta, pdu))
        return l

    def read_sms_msg(self, index):
        r = self.send_cmd('AT+CMGR=%d' % index)
        meta = self.parse_resp_line(r[0])[1]
        if meta == '0,,0':
            return None
        return dict(zip(['stat', 'alpha', 'len'], meta.split(','))), r[1]

    def send_sms_msg(self, pdu_semioctets):
        octets = len(pdu_semioctets)/2 - 1
        self.modem.write('AT+CMGS=%d\r\n' % octets)
        # need to wait for modem to respond to sms request
        select.select([],[],[], 0.5)
        self.modem.write(pdu_semioctets)
        self.send_ctrl_z()

        r = self.modem.read_until(self.CMD_RET,
                                  self.modem.cfg.timeout['send_pdu'])
        r = [i.strip() for i in r.split('\r\n') if i.strip() != '']
        if r[-1] == 'ERROR':
            raise ModemError('Error executing: "%s"' % cmd)
        return self.parse_resp_line(r[1])[1]

    def set_sms_storage_loc(self, mem1, mem2 = None, mem3 = None):
        cmd = 'AT+CPMS=%s' % mem1
        if mem2: cmd = cmd + ',' + mem2
        if mem3: cmd = cmd + ',' + mem3
        self.send_cmd(cmd)

    def get_sms_storage_info(self):
        r = self.send_cmd('AT^SLMS')
        r = [self.parse_resp_line(l)[1].split(',') for l in r]
        return [(t[0], int(t[1]), int(t[2])) for t in r]
